﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using System.Text;
using System.Threading.Tasks;

namespace Muel.TypeScriptFacade.Core
{
    public abstract class TypeScriptType
    {
        protected Type type;
        protected TypeScriptServiceManager manager;
        
        internal TypeScriptType(TypeScriptServiceManager manager, Type type)
        {
            this.manager = manager;
            this.type = type;
            this.Namespace = type.Namespace;
            this.FullName = type.FullName;
            this.IsPrimitiveBased = type.IsUnderlyingPrimitive();
            this.IsNullable = !type.IsValueType || (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>));
            this.IsAbstract = false;
            this.IsAny = type.IsDictionary() || manager.Settings.ShouldTreatAsAny(type);
            this.IsGeneric = type.IsGenericType && type.GetGenericTypeDefinition() != typeof(Nullable<>) && !type.IsCollection();
            this.Name = (this.IsGeneric)
                ?
                string.Format(
                    "{0}<{1}>",
                    type.Name.Substring(0, type.Name.IndexOf('`')),
                    string.Join(", ", type.GetGenericArguments().Select(arg => arg.Name).ToArray()))
                :
                type.Name;

            // sort out generic type parameters
            if (this.IsGeneric)
            {
                Type[] genericArguments = type.GetGenericArguments();
                this.GenericParameters = genericArguments.Select(arg => manager.ObjectManager.Create(arg)).ToArray();
            }

            this.TypeScriptClassName = (this.IsGeneric)
                ?
                string.Format(
                    "{0}<{1}>",
                    type.Name.Substring(0, type.Name.IndexOf('`')),
                    string.Join(", ", this.GenericParameters.Select(arg => arg.Name + ((arg.BaseType == null) ? string.Empty : string.Format(" extends {0}", arg.BaseType.FullName))).ToArray()))
                :
                type.Name;

            this.TypeScriptTypeName = (this.IsGeneric)
                ?
                string.Format(
                    "{0}<{1}>",
                    type.Name.Substring(0, type.Name.IndexOf('`')),
                    string.Join(", ", this.GenericParameters.Select(arg => (arg.IsGenericParameter) ? arg.Name : string.Format("{0}.{1}", arg.Namespace, arg.TypeScriptTypeName))))
                :
                type.Name;

            if (type.IsGenericParameter)
            {
                this.IsGenericParameter = true;
            }

            // sort out array types
            if (type.IsCollection())
            {
                Type elementType = type.GetCollectionTypeArgument();
                this.ElementType = manager.ObjectManager.Create(elementType);
                this.IsArray = true;
                this.OwnMembers = new TypeScriptProperty[0];
                this.IsGeneric = elementType.IsGenericType;
            }
            else if (type.IsDictionary())
            {
                this.ElementType = manager.ObjectManager.Create(typeof(object));
                this.IsArray = false;
                this.OwnMembers = new TypeScriptProperty[0];
            }
            else
            {
                var ownPropertiesFlags = BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public;
                var inheritedPropertiesFlags = ~BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public;

                IEnumerable<PropertyInfo> ownProperties = GetProperties(ownPropertiesFlags);
                IEnumerable<PropertyInfo> inheritedProperties = GetProperties(inheritedPropertiesFlags);

                // note: we have to defer the initialisation of the properties to avoid infinite loops/stack overflow from 
                // properties which reference a class which references this class. Instead of doing this recursively we use
                // an initialisation loop that fails after a given number of loops - this allows better debugging and error
                // reporting given that loops can be caught before they crash Visual Studio
                this.OwnMembers = ownProperties.Select(prop => new TypeScriptProperty(manager, prop, false)).ToArray();
                this.InheritedMembers = inheritedProperties.Select(prop => new TypeScriptProperty(manager, prop, true)).ToArray();
                this.IsArray = false;
                this.ElementType = this;
                this.IsAbstract = type.IsAbstract;
            }
        }

        public bool IsGenericParameter { get; private set; }

        private IEnumerable<PropertyInfo> GetProperties(BindingFlags flags)
        {
            return type.GetProperties(flags).Where(prop => prop.HasAttributeOfType<DataMemberAttribute>());
        }

        /// <summary>
        /// Determines whether the TypeScriptObject will be created as a TypeScript array
        /// </summary>
        public bool IsArray { get; private set; }

        /// <summary>
        /// Determines whether the TypeScriptObject will be created as an any variable
        /// </summary>
        public bool IsAny { get; private set; }

        /// <summary>
        /// Determines whether the TypeScritpObject is for a built-in type (mscorlib) or is a collection of such types
        /// </summary>
        public bool IsPrimitiveBased { get; private set; }

        /// <summary>
        /// Determines whether the TypeScriptObject is based on a Nullable<> type
        /// </summary>
        public bool IsNullable { get; private set; }

        /// <summary>
        /// Indicates whether the underlying type is abstract
        /// </summary>
        public bool IsAbstract { get; private set; }

        #region Generics stuff
        
        /// <summary>
        /// Indicates whether the underlying type is generic
        /// </summary>
        public bool IsGeneric { get; private set; }

        /// <summary>
        /// Gets the generic parameters for the type in question
        /// </summary>
        public TypeScriptType[] GenericParameters { get; private set; }

        /// <summary>
        /// Gets the TypeScript class name
        /// </summary>
        public string TypeScriptClassName { get; private set; }

        /// <summary>
        /// Gets the TypeScript type name as it should be referenced by other types
        /// </summary>
        public string TypeScriptTypeName { get; set; }

        #endregion


        public string Name { get; private set; }
        public string Namespace { get; private set; }
        public string FullName { get; private set; }
        public TypeScriptProperty[] OwnMembers { get; private set; }
        public TypeScriptProperty[] InheritedMembers { get; private set; }
        public IEnumerable<TypeScriptProperty> AllMembers
        {
            get
            {
                return (InheritedMembers != null) ?
                    this.OwnMembers.Concat(InheritedMembers)
                    :
                    this.OwnMembers;
            }
        }

        public TypeScriptModule Module { get { return manager.ObjectManager.GetModule(this.Namespace); } }        
        public TypeScriptType ElementType { get; private set; }
        internal Type Type { get { return type; } }

        #region Output to TypeScript

        internal abstract string ToTypeScript();
        public abstract bool CanSnapshot { get; }

        public string GetTypeScriptType()
        {
            Type convertibleType = type;
            if (convertibleType.IsGenericType && convertibleType.GetGenericTypeDefinition() == typeof(Nullable<>))
            {
                convertibleType = type.GenericTypeArguments.First();
            }

            if (this.IsArray)
            {
                return string.Format("{0}[]", this.ElementType.GetTypeScriptType());
            }
            else if (convertibleType.IsDictionary() || this.IsAny)
            {
                return "any";
            }
            else if (convertibleType.IsNumeric() && !convertibleType.IsEnum)
            {
                return "number";
            }
            else if (convertibleType.IsEquivalentTo(typeof(string)))
            {
                return "string";
            }
            else if (convertibleType.IsEquivalentTo(typeof(bool)))
            {
                return "boolean";
            }
            else if (convertibleType.IsEquivalentTo(typeof(DateTime)))
            {
                return "string";
            }
            else if (convertibleType.IsEquivalentTo(typeof(Guid)))
            {
                return "string";
            }
            else if (convertibleType.IsEquivalentTo(typeof(char)))
            {
                return "string";
            }
            else if (convertibleType.IsEquivalentTo(typeof(void)))
            {
                return "void";
            }
            else if (convertibleType.IsGenericParameter)
            {
                return this.Name;
            }
            else
            {
                return string.Format("{0}.{1}",
                    this.Namespace,
                    this.TypeScriptTypeName);
            }
        }

        #endregion

        #region Inheritance

        internal abstract void InitialiseInheritance();

        public TypeScriptType BaseType { get; protected set; }
        public TypeScriptType[] ChildTypes { get; protected set; }

        internal IEnumerable<TypeScriptType> GetDescendentTypes()
        {
            return this.ChildTypes
                .Concat(this.ChildTypes.SelectMany(t => t.GetDescendentTypes()))
                .Distinct();
        }

        #endregion

        /// <summary>
        /// For multi-dimensional collections, keeps stepping up through the ElementType until it reaches a non-collection type
        /// </summary>
        /// <returns>A non-array element type</returns>
        internal TypeScriptType GetNonCollectionElementType()
        {
            TypeScriptType elementType = this.ElementType;
            while (elementType.ElementType != elementType)
            {
                elementType = elementType.ElementType;
            }
            return elementType;
        }

        public override string ToString()
        {
            return this.FullName;
        }
        
        internal static TypeScriptType Create(TypeScriptServiceManager manager, System.Type type)
        {
            if (type.IsEnum)
            {
                return new TypeScriptEnum(manager, type);
            }
            else
            {
                return new TypeScriptClass(manager, type);
            }
        }

        /// <summary>
        /// Indicates whether the type should be emitted in the generated TypeScript
        /// </summary>
        public bool ShouldEmit
        {
            get
            {
                return !this.IsPrimitiveBased &&
                    !this.IsArray &&
                    !this.IsAny &&
                    !this.IsGenericParameter &&
                    !this.type.IsConstructedGenericType &&
                    (!this.IsGeneric || this.GenericParameters.All(param => param.IsGenericParameter));
            }
        }

        public int NestingDepth { get { return (this.BaseType == null) ? 0 : this.BaseType.NestingDepth + 1; } }

    }
}